package cn.boodqian.airreport;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.ProgressDialog;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import cn.boodqian.util.Log;
import cn.boodqian.util.ServerMessageTask;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Button;
import android.widget.Toast;
import android.widget.ViewFlipper;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import cn.boodqian.util.ObjectSerializer;

import com.google.ads.*;

public class AirreportActivity extends Activity {
	class MyGestureDetectorListener extends SimpleOnGestureListener {
		private static final int FLING_MIN_DISTANCE = 30;
		private static final int FLING_MAX_DISTANCE = 200;
		private static final int FLING_THRESHOLD_VELOCITY = 5;

		@Override
		public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
				float velocityY) {
			if (e1.getX() - e2.getX() > FLING_MIN_DISTANCE
					&& Math.abs(velocityX) > FLING_THRESHOLD_VELOCITY) {
				flipper.setInAnimation(AnimationUtils.loadAnimation(
						AirreportActivity.this, R.anim.slide_right_in));
				flipper.setOutAnimation(AnimationUtils.loadAnimation(
						AirreportActivity.this, R.anim.slide_left_out));
				flipper.showNext();
			} else if (e2.getX() - e1.getX() > FLING_MIN_DISTANCE
					&& Math.abs(velocityX) > FLING_THRESHOLD_VELOCITY) {
				flipper.setInAnimation(AnimationUtils.loadAnimation(
						AirreportActivity.this, R.anim.slide_left_in));
				flipper.setOutAnimation(AnimationUtils.loadAnimation(
						AirreportActivity.this, R.anim.slide_right_out));
				flipper.showPrevious();
			}
			return false;
		}
	}
	
	class ServerMessageHandler extends Handler {
		/* (non-Javadoc)
		 * @see android.os.Handler#handleMessage(android.os.Message)
		 */
		@Override
		public void handleMessage(Message msg) {
			long when = System.currentTimeMillis();
			int currentVersion = packageInfo.versionCode;
			int messageVersion = 0;
			int savedLastServerMessageCode = mLastServerMessageCode;
			try{
				HashMap<String, String> map = (HashMap<String, String>)msg.obj;

				messageVersion = Integer.parseInt(map.get("version"));
				Log.i(String.format("Server Message rcv, %d (%d)", messageVersion, currentVersion));
				
				if(currentVersion>messageVersion) return;
				
				String message;
				String title;
				int messageCode=0;
				if(currentVersion<messageVersion) {
					// When version below, show 'update' field
					title = AirreportActivity.this.getString(R.string.server_update);
					message = map.get("update");
				} else {
					// When version matches, show 'message' field
					title = AirreportActivity.this.getString(R.string.server_message);
					message = map.get("message");
					String str = map.get("message-code");
					if(str!=null)
						messageCode = Integer.parseInt(str);
					
					// Only display message with higher code
					if(mLastServerMessageCode >= messageCode) return;
					mLastServerMessageCode = messageCode;
				}
				if(message==null || message.compareTo("")==0) return;
				
				Intent intent = new Intent(AirreportActivity.this, ServerMessageActivity.class);
				intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
				intent.putExtra("title", title);
				intent.putExtra("message", message);
				PendingIntent contentIntent = PendingIntent.getActivity(AirreportActivity.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT );
				Notification notification = new Notification(cn.boodqian.airreport.R.drawable.ic_launcher, title, when);
				notification.flags = Notification.FLAG_AUTO_CANCEL;
				NotificationManager nm = (NotificationManager) AirreportActivity.this.getSystemService(NOTIFICATION_SERVICE);
				notification.setLatestEventInfo(AirreportActivity.this, title, message, contentIntent);

				nm.notify(1, notification);
			} catch (Exception e) {
				if(currentVersion == messageVersion)
					mLastServerMessageCode = savedLastServerMessageCode;
				e.printStackTrace();
			}
		}
		
	}

	private static final String TAG = "AirreportActivity";;

	private ServerMessageHandler handler = new ServerMessageHandler();
	private AirDataProvider airdata_provider = new ShanghaiAirDataProvider();
	private SharedPreferences mPrefs;
	private PackageInfo packageInfo;
	private int mHistoryMode = 1; // 1 - 24 hour, 2 - 30 days

	// Persistent Data
	private HashMap<String, AirData> mAirdataHash = null;
	private HashMap<String, Integer> mLocations = null;
	private String mCurrentLocation = "";
	private int mLastServerMessageCode = 0;

	private AirData mNewAirdata = null;

	// UI Data
	private Float realtime_pm25_us_v = 0f;
	private Float avg24_pm25_us_v = 0f;
	private Float realtime_pm25_v = 0f;
	private Float avg24_pm25_v = 0f;
	private Float realtime_pm10_v = 0f;
	private Float avg24_pm10_v = 0f;
	private Float realtime_so2_v = 0f;
	private Float avg24_so2_v = 0f;
	private Float realtime_no2_v = 0f;
	private Float avg24_no2_v = 0f;

	// UI Controllers
	private TextView location_name;
	private TextView history_view_title;
	private ViewFlipper flipper;
	private HistoryView history_view;

	private GestureDetector mGestureDetector;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.flipper);
		
		location_name = (TextView) findViewById(R.id.location_name);
		history_view_title = (TextView) findViewById(R.id.history_view_title);
		history_view = (HistoryView) findViewById(R.id.history_view);
		flipper = (ViewFlipper) findViewById(R.id.flipper);

		registerForContextMenu(location_name);
		registerForContextMenu(history_view_title);
		/*
		history_view_title.setOnClickListener(
				new View.OnClickListener() {
					
					@Override
					public void onClick(View v) {
						if(mHistoryMode == 1) mHistoryMode = 2;
						else if(mHistoryMode == 2) mHistoryMode = 1;
						
						updateHistoryTitle();
						updateUI();
					}
				}
				);
		*/

		mGestureDetector = new GestureDetector(new MyGestureDetectorListener());

		try {
			packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
		} catch (NameNotFoundException e1) {
			packageInfo = null;
			Log.e("Cannot get package info:\n"+e1.toString());
		}
		
		restorePrefs();

		if (mAirdataHash == null) {
			mAirdataHash = new HashMap<String, AirData>();
		}

		if (mLocations != null)
			airdata_provider.setLocations(mLocations);

		if (mCurrentLocation == null) {
			// First use
			new AlertDialog.Builder(this)
					.setTitle(getString(R.string.usage))
					.setMessage(
							getString(R.string.first_use) + "\n\n"
									+ getString(R.string.usage_content))
					.setPositiveButton(getString(R.string.close),
							new DialogInterface.OnClickListener() {
								@Override
								public void onClick(DialogInterface dialog,
										int id)

								{
									updateAll();
								}
							}).setIcon(R.drawable.ic_launcher).show();
		} else {
			location_name.post(new Runnable() {
				@Override
				public void run() {
					updateAll();
				}
			});
		}
	}

	private void restorePrefs()
	{
		mPrefs = getPreferences(MODE_PRIVATE);
		String version = mPrefs.getString("version", "");
		if(packageInfo.versionName.compareTo(version)!=0) {
			Log.i(String.format("Version names (%s vs %s) do not match",version,packageInfo.versionName));
			mCurrentLocation = null;
			mLocations  = null;
			mAirdataHash = null;
			return;
		}
		
		mCurrentLocation = mPrefs.getString("location", null);
		if (mCurrentLocation != null) {
			if(Log.isLoggable(Log.DEBUG)) Log.d("current location deserialized");
			if(Log.isLoggable(Log.VERBOSE))  Log.v(mCurrentLocation);
		}

		try {
			mLocations = (HashMap<String, Integer>) ObjectSerializer
					.deserialize(mPrefs.getString("locations", null));
			if (mLocations != null) {
				if(Log.isLoggable(Log.DEBUG)) Log.d("locations deserialized");
				if(Log.isLoggable(Log.VERBOSE))  Log.v(mLocations.toString());
			}
		} catch (Exception e) {
			mLocations = null;
			e.printStackTrace();
		}

		try {
			mAirdataHash = (HashMap<String, AirData>) ObjectSerializer
					.deserialize(mPrefs.getString("airdata", null));
			if (mAirdataHash != null) {
				if(Log.isLoggable(Log.DEBUG)) Log.d( "airdata deserialized");
				for (HashMap.Entry<String, AirData> entry : mAirdataHash.entrySet()) {
					String key = entry.getKey();
					AirData value = entry.getValue();
					if(Log.isLoggable(Log.DEBUG)) Log.d( "Data deserialized: " + key + ":" + value.getLength());
					if(Log.isLoggable(Log.DEBUG)) Log.d( "Update Time: " + value.getTime());
				}
			}
		} catch (Exception e) {
			mAirdataHash = null;
			e.printStackTrace();
		}
		mLastServerMessageCode = mPrefs.getInt("last_server_message_code", 0);
	}
	@Override
	public void onCreateContextMenu(ContextMenu menu, View v,
			ContextMenuInfo menuInfo) {
		super.onCreateContextMenu(menu, v, menuInfo);

		if (v == location_name || v == history_view_title) {
			if (mLocations == null) { // Context menu called when locations not
										// ready
				return;
			}
			Iterator<?> iter = mLocations.entrySet().iterator();
			while (iter.hasNext()) {
				HashMap.Entry<?, ?> entry = (HashMap.Entry<?, ?>) iter.next();
				menu.add(0, 0, 0, (String) entry.getKey());
			}
		} else {
			MenuInflater inflater = getMenuInflater();
			inflater.inflate(R.menu.data_context, menu);
		}
	}

	@Override
	public boolean onContextItemSelected(MenuItem item) {
		if (item.getItemId() == 0) {
			String location_title = item.getTitle().toString();
			if (changeLocation(location_title))
				updateAirdata(null);
		} else {
			switch (item.getItemId()) {
			case R.id.menu_refresh:
				updateAll();
				break;
			case R.id.menu_history:
				flipper.setDisplayedChild(1);
				break;
			default:
				return super.onContextItemSelected(item);
			}
		}
		return true;
	}

	/**
	 * Name: changeLocations
	 * Purpose: Change to a new location
	 * Assumption: mLocations has value, caller need to call updateAirdata to update the airdata
	 */
	private boolean changeLocation(String location) {
		Log.i(location);
		String old = location_name.getText().toString();
		location_name.setText(location);
		if (airdata_provider.setLocation(location)) {
			mCurrentLocation = location;
			updateHistoryTitle();
			return true;
		} else {
			Toast.makeText(this, "Cannot change to location " + location,
					Toast.LENGTH_LONG).show();
			airdata_provider.setLocation(old);
			return false;
		}
	}

	/**
	 * Name: updateLocations
	 * Parameter: A runnable to run after we get the locations
	 * Purpose: Retrieve and update all the air locations from server
	 * Assumption: mLocations is null or out of date
	 * Description: mLocations will be updated in a new created thread
	 */
	private void updateLocations(final Runnable run) {
		Log.i("enter");
		final ProgressDialog dlg = ProgressDialog.show(this,
				getString(R.string.refresh_wait),
				getString(R.string.refresh_location), true);

		// Use thread to prevent any possible UI freezes
		Thread thd = new Thread() {
			@Override
			public void run() {
				mLocations = airdata_provider.getLocations();
				handler.post(new Runnable() {
					@Override
					public void run() {
						if (mLocations == null) {
							Toast.makeText(
									dlg.getContext(),
									getString(R.string.refresh_error) + "\n"
											+ airdata_provider.getLastError(),
									Toast.LENGTH_LONG).show();
						} else {
							Toast.makeText(dlg.getContext(),
									getString(R.string.refresh_done),
									Toast.LENGTH_LONG).show();
						}
						dlg.dismiss();

						if (run != null)
							run.run();
					}
				});
			}
		};
		thd.start();
	}

	private Float calc24HourAverage(List<Float> list, int index, int length) {
		assert(index+length-1<list.size());
		float sum = 0.0f;
		int num = 0;
		for(int i=0;i<length;i++) {
			float value = list.get(index+i);
			if (Float.compare(value, 0f) > 0) {
				sum += value;
				num++;
			}
		}
		return num>0 ? (sum/num) : 0f;
	}
	
	private void calc24HourAverage() {
		AirData airdata = mAirdataHash.get(mCurrentLocation);
		if (airdata == null) {
			realtime_pm25_us_v = 0f;
			avg24_pm25_us_v = 0f;
			realtime_pm25_v = 0f;
			avg24_pm25_v = 0f;
			realtime_pm10_v = 0f;
			avg24_pm10_v = 0f;
			realtime_so2_v = 0f;
			avg24_so2_v = 0f;
			realtime_no2_v = 0f;
			avg24_no2_v = 0f;
			return;
		}

		float sum24_pm25_us = 0.0f;
		float sum24_pm25 = 0.0f;
		float sum24_pm10 = 0.0f;
		float sum24_so2 = 0.0f;
		float sum24_no2 = 0.0f;
		int n_pm25_us = 0, n_pm25 = 0, n_pm10 = 0, n_so2 = 0, n_no2 = 0;
		for (int i = 0; i < Math.min(24, airdata.getLength()); i++) {
			int index = airdata.getLength() - i - 1;
			float t;
			t = airdata.getPM25_US().get(index);
			if (Float.compare(t, 0f) > 0) {
				sum24_pm25_us += t;
				n_pm25_us++;
			}
			t = airdata.getPM25().get(index);
			if (Float.compare(t, 0f) > 0) {
				sum24_pm25 += t;
				n_pm25++;
			}
			t = airdata.getPM10().get(index);
			if (Float.compare(t, 0f) > 0) {
				sum24_pm10 += t;
				n_pm10++;
			}
			t = airdata.getSO2().get(index);
			if (Float.compare(t, 0f) > 0) {
				sum24_so2 += t;
				n_so2++;
			}
			t = airdata.getNO2().get(index);
			if (Float.compare(t, 0f) > 0) {
				sum24_no2 += t;
				n_no2++;
			}
		}
		avg24_pm25_us_v = (n_pm25_us > 0 ? sum24_pm25_us / n_pm25_us : 0);
		avg24_pm25_v = (n_pm25 > 0 ? sum24_pm25 / n_pm25 : 0);
		avg24_pm10_v = (n_pm10 > 0 ? sum24_pm10 / n_pm10 : 0);
		avg24_so2_v = (n_so2 > 0 ? sum24_so2 / n_so2 : 0);
		avg24_no2_v = (n_no2 > 0 ? sum24_no2 / n_no2 : 0);
		
		int i=0;
		i=airdata.getLength() - 1;
		do {
			realtime_pm25_us_v = airdata.getPM25_US().get(i--);
		} while(realtime_pm25_us_v.compareTo(0f)==0 && i>=0);
		i=airdata.getLength() - 1;
		do {
			realtime_pm25_v = airdata.getPM25().get(i--);
		} while(realtime_pm25_v.compareTo(0f)==0 && i>=0);
		i=airdata.getLength() - 1;
		do {
			realtime_pm10_v = airdata.getPM10().get(i--);
		} while(realtime_pm10_v.compareTo(0f)==0 && i>=0);
		i=airdata.getLength() - 1;
		do {
			realtime_so2_v = airdata.getSO2().get(i--);
		} while(realtime_so2_v.compareTo(0f)==0 && i>=0);
		i=airdata.getLength() - 1;
		do {
			realtime_no2_v = airdata.getNO2().get(i--);
		} while(realtime_no2_v.compareTo(0f)==0 && i>=0);
	}

	/**
	 * Name: updateAirdata
	 * 
	 * Parameter: A runnable to run after we get the airdata
	 * 
	 * Purpose: Retrieve and update the airdata of current location
	 * 
	 * Assumption: mLocations and mCurrentLocation has value
	 * 
	 * Description: if mLocations is out of date (return error from server)
	 * caller need to call getLocations again to update
	 */
	// TODO if mLocations is out of date (return error from server) caller need
	// to call getLocations again to update
	private void updateAirdata(final Runnable run) {
		Log.i("enter");

		// If we already get data, check the time stamp to see whether we need
		// to retrieve from server
		// and calculate how much data we need if yes
		long timediff = 0;
		int hours = AirData.MAX_HOURS;
		AirData airdata = mAirdataHash.get(mCurrentLocation);
		if (airdata != null) {
			timediff = new Date().getTime() - airdata.getTime().getTime();
			timediff /= 1000 * 60 * 60; // difference in hours
			if(Log.isLoggable(Log.DEBUG)) Log.d( "timediff: " + timediff);
			
			// No need to refresh if less than an hour
			if (timediff == 0) {
				Toast.makeText(this, getString(R.string.refresh_toosoon),
						Toast.LENGTH_LONG).show();
				calc24HourAverage();
				updateUI();
				return;
			}

			hours = 0;
			int i = airdata.getLength() - 1;
			while (i >= 0) {
				if (airdata.getPM10().get(i).compareTo(0f) == 0) {
					hours++;
					i--;
				}
				break;
			}
			hours += timediff;
		}

		final int hoursNeeded = hours;
		Log.i( "Hours Needed to refresh: " + hoursNeeded);

		final ProgressDialog dlg = ProgressDialog.show(this,
				getString(R.string.refresh_wait),
				getString(R.string.refresh_data), true);
		// Use thread to prevent any possible UI freezes
		new Thread() {
			@Override
			public void run() {
				mNewAirdata = airdata_provider.getAirData(hoursNeeded);
				handler.post(new Runnable() {
					@Override
					public void run() {
						if (mNewAirdata == null) {
							Toast.makeText(
									dlg.getContext(),
									getString(R.string.refresh_error) + "\n"
											+ airdata_provider.getLastError(),
									Toast.LENGTH_LONG).show();
						} else {
							AirData airdata = mAirdataHash
									.get(mCurrentLocation);
							if (airdata == null) {
								mAirdataHash.put(mCurrentLocation, mNewAirdata);
							} else
								airdata.merge(mNewAirdata);
							Toast.makeText(dlg.getContext(),
									getString(R.string.refresh_done),
									Toast.LENGTH_LONG).show();
						}
						updateUI();
						dlg.dismiss();
						
						if(run != null) run.run();
					}
				});
			}
		}.start();
	}

	private void updateAQIText(int viewId, float value, int aqi) {
		TextView t = (TextView) findViewById(viewId);
		if (value < 1e-10) {
			t.setText(getString(R.string.not_available));
			t.setTextColor(Color.GRAY);
		} else {
			t.setText(String.format("%.3f/%03d:%s", value, aqi,
					getString(AQI.AQICategory(aqi))));
			t.setTextColor(Color.parseColor(AQI.AQIColor(aqi)));
		}
	}

	/**
	 * Name: updateUI Purpose: updateUI according to the data in mAirdataHash
	 * Assumption: None (airdata of current location can be null)
	 */
	private void updateUI() {
		AirData airdata = mAirdataHash.get(mCurrentLocation);

		calc24HourAverage();

		TextView t = (TextView) findViewById(R.id.update_time);
		if (airdata != null) {
			DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:00");
			t.setText(getString(R.string.update_time) + " "
					+ df.format(airdata.getTime()));
		} else {
			t.setText(getString(R.string.update_time) + " "
					+ getString(R.string.not_available));
		}

		updateAQIText(R.id.realtime_pm25_us, realtime_pm25_us_v,
				AQI.PM25(realtime_pm25_us_v));
		updateAQIText(R.id.avg24_pm25_us, avg24_pm25_us_v, AQI.PM25(avg24_pm25_us_v));
		updateAQIText(R.id.realtime_pm25, realtime_pm25_v,
				AQI.PM25(realtime_pm25_v));
		updateAQIText(R.id.avg24_pm25, avg24_pm25_v, AQI.PM25(avg24_pm25_v));
		updateAQIText(R.id.realtime_pm10, realtime_pm10_v,
				AQI.PM10(realtime_pm10_v));
		updateAQIText(R.id.avg24_pm10, avg24_pm10_v, AQI.PM10(avg24_pm10_v));
		updateAQIText(R.id.realtime_so2, realtime_so2_v,
				AQI.SO2_1hr(realtime_so2_v));
		updateAQIText(R.id.avg24_so2, avg24_so2_v, AQI.SO2_24hr(avg24_so2_v));
		updateAQIText(R.id.realtime_no2, realtime_no2_v,
				AQI.NO2(realtime_no2_v));
		updateAQIText(R.id.avg24_no2, avg24_no2_v, AQI.NO2(avg24_no2_v));

		// Convert all data to AQI
		ArrayList<Float> hist_pm25_us = new ArrayList();
		ArrayList<Float> hist_pm25 = new ArrayList();
		ArrayList<Float> hist_pm10 = new ArrayList();
		ArrayList<Float> hist_so2 = new ArrayList();
		ArrayList<Float> hist_no2 = new ArrayList();
		ArrayList<String> marks = new ArrayList();
		
		Log.i("mHistoryMode="+mHistoryMode);
		if(mHistoryMode==1) {
			if (airdata != null) {
				int length = Math.min(24, airdata.getLength());
				int lastHour = airdata.getTime().getHours();
				for (int i = 0; i < length; i++) {
					int index = airdata.getLength()-length+i;
					hist_pm25_us.add((float) AQI
							.PM25((Float) airdata.getPM25_US().get(index)));
					hist_pm25.add((float) AQI
							.PM25((Float) airdata.getPM25().get(index)));
					hist_pm10.add((float) AQI
							.PM10((Float) airdata.getPM10().get(index)));
					hist_so2.add((float) AQI.SO2_1hr((Float) airdata.getSO2()
							.get(index)));
					hist_no2.add((float) AQI.NO2((Float) airdata.getNO2().get(index)));

					int hour = lastHour - length + 1 + i;
					if(hour<0) hour+=24;
					marks.add(String.format("%02d", hour));
				}
			}
		} else if (mHistoryMode==2) {
			if (airdata != null) {
				int length = 30;
				int lastHour = airdata.getTime().getHours();
				Calendar cal = Calendar.getInstance();
				cal.setTime(airdata.getTime());
				int lastDay = cal.get(Calendar.DAY_OF_MONTH);
				Log.i("lastDay="+lastDay+",lastHour="+lastHour+", totallength="+airdata.getLength());
				for (int i = 0; i < length; i++) {
					int index = airdata.getLength()-1-lastHour-24*(length-i);
					hist_pm25.add((float) AQI.PM25(
							calc24HourAverage(airdata.getPM25(), index, 24)
							));
					hist_pm10.add((float) AQI.PM10(
							calc24HourAverage(airdata.getPM10(), index, 24)
							));
					hist_so2.add((float) AQI.SO2_24hr(
							calc24HourAverage(airdata.getSO2(), index, 24)
							));
					hist_no2.add((float) AQI.NO2(
							calc24HourAverage(airdata.getNO2(), index, 24)
							));

					int day = lastDay-(length-i);
					if(day<=0) day+=30;
					marks.add(String.format("%02d",day));
				}
			}
		}
		
		// Display the curve
		/* http://www.wahart.com.hk/rgb.htm */
		history_view.clearHistoryData();
		if(marks.size()>0) history_view.setHorizonMark(marks);
		history_view.addHistoryData(hist_pm25_us, 0xffCDAF95, "PM2.5(US)"); // CornflowerBlue
		history_view.addHistoryData(hist_pm25, 0xff6495ED, "PM2.5"); // CornflowerBlue
		history_view.addHistoryData(hist_pm10, 0xffFFD700, "PM10"); // Gold
		history_view.addHistoryData(hist_so2, 0xff2E8B57, "SO2"); // SeaGreen
		history_view.addHistoryData(hist_no2, 0xffcd5c5c, "NO2"); // IndianRed
		history_view.invalidate();
	}

	private void updateAll() {
		Log.i( "enter");
		Runnable run = new Runnable() {
			@Override
			public void run() {
				if (mLocations != null) { /* updateLocations could be failed */
					if (mCurrentLocation == null) {
						openContextMenu(location_name);
					} else {
						if (changeLocation(mCurrentLocation))
							updateAirdata(null);
					}
				}
			}
		};
		if (mLocations == null)
			updateLocations(run);
		else
			run.run();
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.airreport, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		// Handle item selection
		switch (item.getItemId()) {
		case R.id.menu_refresh:
			updateAll();
			return true;
		case R.id.menu_location:
			openContextMenu(location_name);
			return true;
		case R.id.menu_aqilevel:
			flipper.setDisplayedChild(2);
			return true;
		case R.id.menu_history:
			flipper.setDisplayedChild(1);
			return true;
		case R.id.menu_usage:
			new AlertDialog.Builder(this).setTitle(getString(R.string.usage))
					.setMessage(getString(R.string.usage_content))
					.setPositiveButton(getString(R.string.close), null).show();
			return true;
		case R.id.menu_about:
			new AlertDialog.Builder(this)
			.setTitle(getString(R.string.about))
			.setMessage(
					String.format("%s v%s\n\n%s\n",
							getString(R.string.app_name),
							packageInfo==null?"unkown":packageInfo.versionName,
							getString(R.string.about_content),
							getString(R.string.data_source)))
							.setPositiveButton(getString(R.string.close), null)
							.setIcon(R.drawable.ic_launcher).show();

			return true;
		default:
			return super.onOptionsItemSelected(item);
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return mGestureDetector.onTouchEvent(event);
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onPause()
	 */
	@Override
	protected void onPause() {
		super.onPause();

		if(Log.isLoggable(Log.DEBUG)) Log.d("enter");
		try {
			Editor editor = mPrefs.edit();
			editor.putString("version", packageInfo.versionName);
			editor.putString("location", mCurrentLocation);
			if(Log.isLoggable(Log.DEBUG)) Log.d( "current location serialized");
			editor.putString("locations",
					ObjectSerializer.serialize(mLocations));
			if(Log.isLoggable(Log.DEBUG)) Log.d( "locations serialized");
			editor.putInt("last_server_message_code", mLastServerMessageCode);
			editor.commit();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onSaveInstanceState(android.os.Bundle)
	 */
	@Override
	protected void onSaveInstanceState(Bundle outState) {
		super.onSaveInstanceState(outState);
	}

	@Override
	protected void onStart() {
		super.onStart();
		
		if(false) {
			// For debug on emulator
			new ServerMessageTask(handler).execute("http://10.0.2.2:3000/airreport");
		} else {
			// Direct use of the domain cause mojolicio crash (unknown error), access by ip is fine
			// so translate to ip address first
			new ServerMessageTask(handler).execute("http://%s:3000/airreport","qianhaib.eicp.net");
		}
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onStop()
	 */
	@Override
	protected void onStop() {
		super.onStop();

		if(Log.isLoggable(Log.DEBUG)) Log.d( "onStop");
		try {
			Editor editor = mPrefs.edit();
			editor.putString("airdata",
					ObjectSerializer.serialize(mAirdataHash));
			if(Log.isLoggable(Log.DEBUG)) Log.d("airdata serialized");
			for (HashMap.Entry<String, AirData> entry : mAirdataHash.entrySet()) {
				String key = entry.getKey();
				AirData value = entry.getValue();
				if(Log.isLoggable(Log.DEBUG)) Log.d(key+":"+value.getLength());
				if(Log.isLoggable(Log.DEBUG)) Log.d(value.getTime().toString());
			}
			editor.commit();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private void updateHistoryTitle() {
		int titleid = (mHistoryMode == 1)?R.string.history:R.string.history_30days;
		history_view_title.setText(String.format("%s(%s)",
				getString(titleid), mCurrentLocation));
	}
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev){
		super.dispatchTouchEvent(ev);    
		return mGestureDetector.onTouchEvent(ev); 
	}

	private boolean isSameDay(long a, long b) {
		return (a/86400000 == b/86400000);
	}
}